package edu.cth.scrabbit.client.core;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;

import edu.cth.scrabbit.client.core.network.ClientNetworkHandler;
import edu.cth.scrabbit.shared.GameLetter;
import edu.cth.scrabbit.shared.GameState;
import edu.cth.scrabbit.shared.Player;
import edu.cth.scrabbit.shared.network.Statement;
import edu.cth.scrabbit.shared.network.Statement.Statement_Type;

/**
 * This class contains all information that is important to represent the 
 * game as it exist on the server
 * 
 * @author Anders Jonsson, Marcus Harryson
 * 
 */
public class ClientModel implements IObservable, IClientModel {
	private PropertyChangeSupport listeners;
	private GameLetter[][] gameBoard;
	private Player[] playerList;
	private int currentPlayer;

	private ClientNetworkHandler listener;

	private static ClientModel model;

	private boolean isHost = false;
	private boolean gameStarted = false;

	private ClientModel() {
		listeners = new PropertyChangeSupport(this);
	}

	// Creates a singelton
	public static ClientModel getInstance() {
		if (model == null) {
			model = new ClientModel();
		}
		return model;
	}
	
	@Override
	public void joinGame(String ip, String nickname, boolean isHost) {
		this.isHost = isHost;
		startListening(ip);

		Statement s = new Statement(Statement_Type.CONNECT, nickname);
		listener.sendStatement(s);

		listener.startPinging();

		fireDisableConnect();
	}

	@Override
	public void startListening(String ip) {
		listener = new ClientNetworkHandler(ip, 4567);
		listener.start();
	}

	@Override
	public void startGame() {
		if (isHost) {
			Statement s = new Statement(Statement_Type.START_GAME, null);
			listener.sendStatement(s);
		}
	}

	@Override
	public void processGameUpdate(GameState gs) {
		gameBoard = gs.getLetterMatrix();

		// The current player value has changed, let the GUI know about this:
		if (gs.getCurrentPlayer() != currentPlayer) {
			fireCurrentPlayerChanged(gs.getCurrentPlayer() == gs
					.getPlayerPosition());
		}

		// Has the game ended?
		if (gameStarted && !gs.hasGameStarted()) {
			// Note that in a tie, this method makes the player first in the
			// playing order the winner
			Player winner = gs.getPlayers()[0];
			int highestScore = Integer.MIN_VALUE;
			for (int i = 0; i < 4; i++) {
				if (gs.getPlayers()[i] != null) {
					int score = gs.getPlayers()[i].getScore();
					if (score > highestScore) {
						winner = gs.getPlayers()[i];
						highestScore = score;
					}
				}
			}
			fireDisablePlay();
			fireGameEnded(winner);

			if (gs.getPlayerPosition() == 0) {
				fireEnableStartGame();
			}
		}

		// When the number of players changes from 1 to 2, enable starting the
		// game (unless you're not the host or the game has already started):
		if (playerList != null) {
			int no1 = logicSize(playerList);
			int no2 = logicSize(gs.getPlayers());
			if (no1 != no2 && no2 >= 2 && isHost && !gs.hasGameStarted()) {
				fireEnableStartGame();
			}
		}

		if (gs.getPlayerPosition() == gs.getCurrentPlayer()) {
			fireEnablePlay();
		} else {
			fireDisablePlay();
		}

		// The game just started:
		if (!this.gameStarted && gs.hasGameStarted()) {
			fireClearGameboard(gs.getLetterMatrix());
			fireDisableStartGame();
		}

		this.gameStarted = gs.hasGameStarted();

		playerList = gs.getPlayers().clone();
		currentPlayer = gs.getCurrentPlayer();
		fireGameState(gs);
	}

	private int logicSize(Player[] players) {
		int count = 0;
		for (Player p : players) {
			if (p != null) {
				count++;
			}
		}
		return count;
	}

	@Override
	public void addObserver(PropertyChangeListener listener) {
		listeners.addPropertyChangeListener(listener);
	}

	@Override
	public void pass() {
		Statement s = new Statement(Statement_Type.PASS, null);
		listener.sendStatement(s);
	}

	@Override
	public void swap() {
		Statement s = new Statement(Statement_Type.SWAP, null);
		listener.sendStatement(s);
	}

	@Override
	public void play(List<GameLetter> placedLetters) {
		// Start by clearing the game board:
		fireClearGameboard(gameBoard);

		if (wordIsInLine(placedLetters)
				&& (noGap(placedLetters) || isFirstGameRound())) {
			// If the attempted move is a valid one, direct it to the server for
			// correction. Else, let the player try again.
			List<GameLetter> word = new ArrayList<GameLetter>();
			for (GameLetter gl : placedLetters) {
				word.add(new GameLetter(gl));
			}

			Statement s = new Statement(Statement_Type.PLAY_WORD, word);
			listener.sendStatement(s);
		}
	}

	private boolean isFirstGameRound() {
		int count = 0;
		for (GameLetter[] row : gameBoard) {
			for (GameLetter gl : row) {
				if (gl.isDummy()) {
					count++;
				}
			}
		}

		if (count == 17 * 17) {
			return true;
		} else {
			return false;
		}
	}

	private boolean noGap(List<GameLetter> placedLetters) {
		// Make sure that at least one letter has a neighbor on the gameboard
		for (GameLetter gl : placedLetters) {
			int x = gl.getXIndex();
			int y = gl.getYIndex();

			// Find a neighbor:
			if (x < 16 && !gameBoard[y][x + 1].isDummy()) {
				return true;
			} else if (x > 0 && !gameBoard[y][x - 1].isDummy()) {
				return true;
			} else if (y < 16 && !gameBoard[y + 1][x].isDummy()) {
				return true;
			} else if (y > 0 && !gameBoard[y - 1][x].isDummy()) {
				return true;
			}
			// None found? try the next letter!
		}

		return false;
	}

	private boolean wordIsInLine(List<GameLetter> placedLetters) {
		// Nonsense parameter checks:
		if (placedLetters == null || placedLetters.isEmpty()) {
			return false;
		}

		// Instantiate highest/lowest values and set them to their respective
		// "negative extremes"
		int highestY = Integer.MIN_VALUE;
		int lowestY = Integer.MAX_VALUE;
		int highestX = Integer.MIN_VALUE;
		int lowestX = Integer.MAX_VALUE;

		// Calculate these values:
		for (GameLetter gl : placedLetters) {
			highestY = highestY > gl.getYIndex() ? highestY : gl.getYIndex();
			highestX = highestX > gl.getXIndex() ? highestX : gl.getXIndex();
			lowestY = lowestY < gl.getYIndex() ? lowestY : gl.getYIndex();
			lowestX = lowestX < gl.getXIndex() ? lowestX : gl.getXIndex();
		}

		if (highestX == lowestX) {
			// We have a vertical word! Make sure there is no gap between
			// letters:
			for (int i = lowestY; i <= highestY; i++) {
				// The current position - this must exist either on the current
				// game board or in the word just placed.
				int x = lowestX;
				int y = i;
				boolean positionExists = !gameBoard[y][x].isDummy();

				for (GameLetter gl : placedLetters) {
					if (gl.hasPosition(x, y)) {
						positionExists = true;
						break;
					}
				}

				if (!positionExists) {
					// We did not find this letter among the placed letters
					return false;
				}
			}

			// We made the check! Return true.
			return true;
		} else if (highestY == lowestY) {
			// We have a horizontal word! Make sure there is no gap between
			// letters:

			for (int i = lowestX; i <= highestX; i++) {
				// The current position - this must exist either on the current
				// game board or in the word just placed.
				int x = i;
				int y = lowestY;
				boolean positionExists = !gameBoard[y][x].isDummy();

				for (GameLetter gl : placedLetters) {
					if (gl.hasPosition(x, y)) {
						positionExists = true;
						break;
					}
				}

				if (!positionExists) {
					// We did not find this letter among the placed letters
					return false;
				}
			}

			// We made the check! Return true.
			return true;
		} else {
			return false;
		}
	}

	@Override
	public void clear() {
		fireClearGameboard(gameBoard);
	}

	@Override
	public void addTemporaryLetter(GameLetter letter) {
		fireLetterUpdate(letter);
	}

	/*
	 * All the following methods are used only to push information to the GUI:
	 */
	private void fireGameState(GameState state) {
		listeners.firePropertyChange("GameState", null, state);
	}

	private void fireLetterUpdate(GameLetter letter) {
		listeners.firePropertyChange("Letter_Update", null, letter);
	}

	private void fireClearGameboard(GameLetter[][] gameboard) {
		listeners.firePropertyChange("Clear_Gameboard", null, gameboard);
	}

	private void fireDisableConnect() {
		listeners.firePropertyChange("Disable_Connect", null, null);
	}

	private void fireEnableStartGame() {
		listeners.firePropertyChange("Enable_Start", null, null);
	}

	private void fireDisableStartGame() {
		listeners.firePropertyChange("Disable_Start", null, null);
	}

	private void fireEnablePlay() {
		listeners.firePropertyChange("Enable_Play", null, null);
	}

	private void fireDisablePlay() {
		listeners.firePropertyChange("Disable_Play", null, null);
	}

	private void fireCurrentPlayerChanged(boolean thisPlayer) {
		listeners.firePropertyChange("New_CurrentPlayer", null, thisPlayer);
	}

	private void fireGameEnded(Player winner) {
		listeners.firePropertyChange("Game_Over", null, winner);
	}
}
